﻿using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Co9320.NamaAlert.Model;

namespace Co9320.NamaAlert.Model
{

	internal partial class LocalDatabaseController : IDisposable
	{
		private delegate tRet lamdba1<tRet, tArg>(tArg Arg);
		private delegate tRet lambda2<tRet, tArg0, tArg1>(tArg0 arg0, tArg1 arg1);
		private delegate tRet lambda3<tRet,tArg0,tArg1,tArg2>(tArg0 Arg0,tArg1 Arg1,tArg2 Arg2);

		private class Pair<tA, tB>
		{
			public Pair(tA A, tB B)
			{
				this.A = A;
				this.B = B;
			}

			public Pair()
			{
				this.A = default(tA);
				this.B = default(tB);
			}

			public tA A
			{
				get;
				set;
			}

			public tB B
			{
				get;
				set;
			}
		}

		#region StaticMethods
		/// <summary>
		/// 指定されたFilePathにSqlCompactDatabaseのファイルを作成し､
		/// テーブルなどを定義します｡
		/// </summary>
		/// <param name="path">作成するFilePath</param>
		/// <param name="isOverWrite">ファイルが存在する場合に､上書きする場合はTrue,それ以外はFalse</param>
		/// <exception cref="IOException">
		/// IsOverWriteにfalseを指定し､尚かつPathに指定されたファイルが存在した場合に発生します｡
		/// </exception> 
		public static void CreateDatabase(string path, bool isOverWrite)
		{
			if (path == null)
			{
				throw new ArgumentException("Path");
			}

			if (isOverWrite && File.Exists(path))
			{
				File.Delete(path);
			}
			else if (!isOverWrite && File.Exists(path))
			{
				return;
			}

			StringBuilder bld = new StringBuilder();

			bld.Append("Data Source=");
			bld.Append(path);
			bld.Append(";");

			using (SqlCeEngine en = new SqlCeEngine(bld.ToString()))
			{
				en.CreateDatabase();
			}

			bld = new StringBuilder();

			using (SqlCeConnection con = new SqlCeConnection(GetConnectionString(path)))
			{
				con.Open();
				CreateTable(con);
				CreateIndex(con);
			}
		}




		/// <summary>
		/// Inex作成
		/// </summary>
		/// <param name="con">Open済みのConnection</param>
		private static void CreateIndex(SqlCeConnection con)
		{
			ConnectionConsistencyCheck(con);


			using (SqlCeCommand cmd = con.CreateCommand())
			{
				//tagsテーブルのINDEX
				//どーもDDLは個別にExecuteする必用有る(まとめてやったら怒られた)
				cmd.CommandText = "CREATE INDEX idx_bind_station_id ON Bind(LiveInfoId);";
				cmd.ExecuteNonQuery();

				cmd.CommandText = "CREATE INDEX idx_bind_tag_id ON Bind(TagsId);";
				cmd.ExecuteNonQuery();

				cmd.CommandText = "CREATE UNIQUE INDEX uq_bind ON Bind(LiveInfoId,TagsId);";
				cmd.ExecuteNonQuery();


				//live_infoテーブル用のINDEX
				//Consider:もしかしたら個別のstation_id fieldとcaster fieldのIndex不要かも知れない｡
				cmd.CommandText = "CREATE INDEX idx_live_info_station_id ON LiveInfo(StationId);";
				cmd.ExecuteNonQuery();

				cmd.CommandText = "CREATE INDEX idx_live_info_caster ON LiveInfo(Caster);";
				cmd.ExecuteNonQuery();

				cmd.CommandText = "CREATE INDEX idx_live_info_ref_time ON LiveInfo(RefTime);";
				cmd.ExecuteNonQuery();

				cmd.CommandText = "CREATE UNIQUE INDEX uq_live ON LiveInfo(StationId,Caster);";
				cmd.ExecuteNonQuery();

			}
		}

		/// <summary>
		/// Table作成
		/// </summary>
		/// <param name="con">Open済みのコネクション</param>
		private static void CreateTable(SqlCeConnection con)
		{
			ConnectionConsistencyCheck(con);

			StringBuilder bld = new StringBuilder();

			using (SqlCeCommand cmd = con.CreateCommand())
			{

				//タグ管理用テーブルのDDL
				//どーもDDLは個別にExecuteする必用有る(まとめてやったら怒られた)
				bld.Append("CREATE TABLE Tags(");
				bld.Append("Id bigint NOT NULL IDENTITY PRIMARY KEY,");
				bld.Append("Value nvarchar(100) NOT NULL UNIQUE,");
				bld.Append("RefTime datetime NOT NULL");
				bld.Append(");");

				cmd.CommandText = bld.ToString();
				cmd.ExecuteNonQuery();
				bld = new StringBuilder();

				//live_info用テーブルのDDL
				bld.Append("CREATE TABLE LiveInfo(");
				bld.Append("Id bigint NOT NULL IDENTITY PRIMARY KEY,");
				bld.Append("StationId nvarchar(100) NOT NULL,");
				bld.Append("Caster nvarchar(100) NOT NULL,");
				bld.Append("RefTime datetime NOT NULL");
				bld.Append(");");

				cmd.CommandText = bld.ToString();
				cmd.ExecuteNonQuery();
				bld = new StringBuilder();

				//bind用テーブルのDDL
				bld.Append("CREATE TABLE Bind(");
				bld.Append("Id bigint NOT NULL IDENTITY PRIMARY KEY,");
				bld.Append("LiveInfoId bigint NOT NULL REFERENCES LiveInfo(id) ON DELETE CASCADE,");
				bld.Append("TagsId bigint NOT NULL REFERENCES tags(id) ON DELETE CASCADE");
				bld.Append(");");

				cmd.CommandText = bld.ToString();
				cmd.ExecuteNonQuery();
				bld = new StringBuilder();

				//このクラスの利用するDataStore
				bld.Append("CREATE TABLE SystemInfo(");
				bld.Append("Id bigint NOT NULL IDENTITY PRIMARY KEY,");
				bld.Append("ParamName nvarchar(100) UNIQUE NOT NULL,");
				bld.Append("ParamValue  nvarchar(100)");
				bld.Append(");");

				cmd.CommandText = bld.ToString();
				cmd.ExecuteNonQuery();
			}

			InitSysInfo(con);
		}

		private static void InitSysInfo(SqlCeConnection con)
		{
			ConnectionConsistencyCheck(con);
			lambda3<object, SqlCeCommand, string, string> Insert = (Command, Name, Value) =>
				{
					Command.Parameters["@Name"].Value = Name;
					Command.Parameters["@Value"].Value = Value;
					Command.ExecuteNonQuery();
					return null;
				};

			using (SqlCeCommand cmd = con.CreateCommand())
			{
				cmd.CommandText = "INSERT INTO SystemInfo(ParamName,ParamValue) VALUES(@Name,@Value);";
				cmd.Parameters.Add("@Name", System.Data.SqlDbType.NVarChar);
				cmd.Parameters.Add("@Value", System.Data.SqlDbType.NVarChar);

				Insert(cmd, "SchemaMajor", "0");
				Insert(cmd, "SchemaMinor", "1");
				Insert(cmd, "RecentDeepShrink", DateTime.Now.ToString());
				Insert(cmd, "RecentShallowShrink", DateTime.Now.ToString());
			}
		}

		/// <summary>
		/// ConnectionStringを生成
		/// </summary>
		/// <param name="Path">開く対象となる.sdfファイルのPath</param>
		/// <returns>生成されたConnectionString</returns>
		private static string GetConnectionString(string Path)
		{
			Trace.Assert(Path != null);
			Trace.Assert(File.Exists(Path));

			return "DATA SOURCE=" + Path + ";";
		}

		/// <summary>
		/// ConnectionのAssertチェック
		/// </summary>
		/// <param name="con">検査対象のConnection</param>
		private static void ConnectionConsistencyCheck(SqlCeConnection con)
		{
			Trace.Assert(con != null);
			Trace.Assert(con.State == System.Data.ConnectionState.Open);
		}


		#endregion

		/// <summary>
		/// Compactに接続するConnectionオブジェクト
		/// </summary>
		private SqlCeConnection Con;

		/// <summary>
		/// 不揮発域の制御クラス｡
		/// </summary>
		private InfoControl info;

		/// <summary>
		/// クラスコンストラクタ｡
		/// </summary>
		/// <param name="Path">
		/// sdfファイルのパスを指定します｡
		/// </param>
		public LocalDatabaseController(string Path)
		{
			//処理前の引数チェック｡
			if (Path == null)
			{
				throw new ArgumentNullException("Path");
			}

			if (!File.Exists(Path))
			{
				throw new IOException("ファイルが存在しません");
			}

			//接続確率
			Con = new SqlCeConnection();
			Con.ConnectionString = GetConnectionString(Path);
			Con.Open();

			//不揮発域コントローラ作成｡
			info = new InfoControl(Con);

			//DBのシュリンク実行｡
			Shrink();
		}



		/// <summary>
		/// マイタグをLocalのDBに追加します｡
		/// </summary>
		/// <param name="StationId">StationID</param>
		/// <param name="Caster">Caster</param>
		/// <param name="Tags">追加すべきタグのリスト</param>
		/// <remarks>
		/// 引数のいずれかが不正な場合は､処理を行わず､そのままReturnします｡
		/// 尚､タグは以下の要件を満たしていない場合､追加されません｡
		/// 1.20文字以内であること(*1)
		/// 2.空白のみで構成されていないこと｡
		/// 3.長さ0の文字列でないこと｡
		/// (*1)サロゲートペアを利用する文字は2文字としてカウントされます｡
		/// </remarks>
		public void Add(string StationId, string Caster, IList<TagModel> Tags)
		{
			try
			{
				//引数整合性チェック｡
				if (StationId == null)
				{
					throw new ArgumentNullException("StationId");
				}

				if (Caster == null)
				{
					throw new ArgumentNullException("Caster");
				}

				if (Tags == null)
				{
					throw new ArgumentNullException("Tags");
				}

				//単純な空文字チェック｡もっかしたら空白のみははじくようにした方が幸せかも知れない｡
				if (StationId == "")
				{
					throw new ArgumentException("StationIdが不正です｡");
				}

				if (Caster == "")
				{
					throw new ArgumentException("Casterが不正です｡");
				}
			}
			catch (Exception)
			{
				return;
			}

			ConnectionConsistencyCheck(Con);


			//live_info.id 取得｡もし､未登録なら､登録する｡
			long LiveInfoId = GetLiveInfoId(StationId, Caster);

			//タグ登録
			List<Pair<long?, string>> TagList = FilterTag(Tags);
			GetTagList(TagList);
			Bind(LiveInfoId, TagList);
		}


		/// <summary>
		/// マイタグをLocalのDBから削除します｡
		/// </summary>
		/// <param name="StationId">StatinId</param>
		/// <param name="Caseter">Caster</param>
		/// <param name="Tags">削除すべきタグのリスト</param>
		/// <remarks>
		/// 引数のいずれかが､不正な場合は処理を行わずReturnします｡
		/// </remarks>
		public void Delete(string StationId, string Caseter, IList<TagModel> Tags)
		{
			try
			{
				if (StationId == null)
				{
					throw new ArgumentNullException("StationID");
				}

				if (Caseter == null)
				{
					throw new ArgumentNullException("Caster");
				}

				if (Tags == null)
				{
					throw new ArgumentNullException("Tags");
				}

				if (StationId == "" || Caseter == "")
				{
					throw new ArgumentException();
				}
			}
			catch (Exception)
			{
				return;
			}

			ConnectionConsistencyCheck(Con);
			//ここでローカルタグの削除処理実行｡


			//LiveInfoId確定
			long LiveInfoId = GetLiveInfoId(StationId, Caseter);

			//Tag加工
			var TagLst = FilterTag(Tags);
			GetTagList(TagLst);

			//Bindテーブルから該当項目除去
			using (SqlCeCommand cmd = Con.CreateCommand())
			{
				cmd.CommandText = "DELETE Bind WHERE LiveInfoId=@LiveInfoId AND TagsID=@TagsId;";
				cmd.Parameters.Add("@LiveInfoID", LiveInfoId);
				cmd.Parameters.Add("@TagsID", System.Data.SqlDbType.BigInt);

				foreach (var elem in TagLst)
				{
					cmd.Parameters["@TagsID"].Value = elem.A.Value;
					cmd.ExecuteNonQuery();
				}
			}


		}

		/// <summary>
		/// DBの不要データの除去をバッチ処理で行います｡
		/// 完全自律です｡
		/// </summary>
		/// <remarks>
		/// このメソッドは､完全自律で実行されます
		/// 実行のトリガとして､
		/// 孤立レコードの除去は30日ごと､
		/// 最終参照日時を元にしたレコードの一括除去は150日ごとに行われ､事前100日間利用の無かったレコードの一括除去を行います｡
		/// また､このメソッド自体のトリガは､コンストラクタ起動時です｡
		/// </remarks>
		private void Shrink()
		{
			ConnectionConsistencyCheck(Con);
			SqlCeCommand cmd = Con.CreateCommand();

			//前回実行日時取得
			DateTime RecentDeep = info.RecentDeepShrink;
			DateTime RecentShallow = info.RecentShallowShrink;


			if (RecentShallow < DateTime.Now.Subtract(new TimeSpan(30, 0, 0, 0)))
			{
				//孤立Tagsの削除
				cmd.CommandText = "DELETE Tags WHERE NOT EXISTS(SELECT * FROM Bind WHERE Bind.TagsID=Tags.ID);";
				cmd.ExecuteNonQuery();


				//孤立LiveInfoの解除
				cmd.CommandText = "DELETE LiveInfo WHERE NOT EXISTS (SELECT * FROM Bind WHERE LiveInfo.ID=Bind.LiveInfoID)";
				cmd.ExecuteNonQuery();

				info.RecentShallowShrink = DateTime.Now;
			}

			if (RecentDeep < DateTime.Now.Subtract(new TimeSpan(150, 0, 0, 0)))
			{
				//閾は100日間未使用｡パラメータにRegist.
				cmd.Parameters.Add("@Time", DateTime.Now.Subtract(new TimeSpan(100, 0, 0, 0)));

				//日付によるTagsの解除
				cmd.CommandText = "DELETE LiveInfo WHERE RefTime<@Time;";
				cmd.ExecuteNonQuery();


				//日付によるLiveInfoの解除
				cmd.CommandText = "DELETE Tags WHERE RefTime<@Time;";
				cmd.ExecuteNonQuery();

				SqlCeEngine eng = null;

				//ファイルのシュリンクを実行｡
				try
				{
					//一時的に接続解除｡
					Con.Close();

					eng = new SqlCeEngine(Con.ConnectionString);
					eng.Shrink();
				}
				finally
				{
					if (eng != null)
					{
						eng.Dispose();
					}
					//再接続｡
					Con.Open();
				}

				info.RecentDeepShrink = DateTime.Now;
			}
		}

		/// <summary>
		/// バインドテーブルに登録します｡
		/// </summary>
		/// <param name="LiveInfoID">対象となるLiveInfoID</param>
		/// <param name="TagList">登録するTagのList</param>
		private void Bind(long LiveInfoID, List<Pair<long?, string>> TagList)
		{
			ConnectionConsistencyCheck(Con);

			lambda2<object,SqlCeCommand, Pair<long?, string>> EachProc =
				(cmd, TagPair) =>
				{
					cmd.Parameters["@TagsID"].Value = TagPair.A.Value;
					cmd.CommandText = "SELECT Id FROM Bind WHERE LiveInfoId=@LiveInfoId AND TagsID=@TagsId;";

					if (cmd.ExecuteScalar() == null)
					{
						cmd.CommandText = "INSERT INTO Bind(LiveInfoID,TagsID) VALUES(@LiveInfoID,@TagsID)";
						cmd.ExecuteNonQuery();
					}

					return null;
				};

			using (SqlCeCommand cmd = Con.CreateCommand())
			{
				cmd.Parameters.Add("@LiveInfoID", LiveInfoID);
				cmd.Parameters.Add("@TagsID", System.Data.SqlDbType.BigInt);

				foreach (var elem in TagList)
				{
					if (elem.A.HasValue)
					{
						EachProc(cmd, elem);
					}
				}
			}
		}

		/// <summary>
		/// TagListを取得します｡
		/// これは､TagとTagsテーブルのIDのPairリストです｡
		/// </summary>
		/// <param name="TagList">登録対象のタグリスト</param>
		/// <remarks>
		/// もし､未登録のタグが含まれている場合は､登録した上で､IDを返します｡
		/// </remarks>
		private void GetTagList(List<Pair<long?, string>> TagList)
		{
			lambda2<long?, SqlCeCommand, string> EachProc =
				(cmd, val) =>
				{
					cmd.Parameters["@Value"].Value = val;
					cmd.Parameters["@Time"].Value = DateTime.Now;

					cmd.CommandText = "UPDATE Tags SET RefTime=@Time WHERE Value=@Value;";
					if ((cmd.ExecuteNonQuery() == 0))
					{
						cmd.CommandText = "INSERT INTO Tags(Value,RefTime) VALUES(@Value,@Time);";
						cmd.ExecuteNonQuery();
					}

					cmd.CommandText = "SELECT Id FROM Tags WHERE Value=@Value;";
					return (long?)cmd.ExecuteScalar();
				};


			Trace.Assert(TagList != null);

			using (SqlCeCommand cmd = Con.CreateCommand())
			{
				cmd.Parameters.Add("@Value", System.Data.SqlDbType.NVarChar);
				cmd.Parameters.Add("@Time", System.Data.SqlDbType.DateTime);

				for (int i = 0; i < TagList.Count; i++)
				{
					if (TagList[i].B != null)
					{
						TagList[i].A = EachProc(cmd, TagList[i].B);
					}
				}
			}

		}

		/// <summary>
		/// Selfタグのみ抽出します｡
		/// </summary>
		/// <param name="Tags">抽出対象のTagList</param>
		/// <returns>抽出されたTagList</returns>
		private List<Pair<long?, string>> FilterTag(IList<TagModel> Tags)
		{
			List<Pair<long?, string>> Ret = new List<Pair<long?, string>>();

			

			IEnumerable<string> FileterEnum = Tags
                .Where(tmp => tmp.Kind == TagKind.Self)
				.Select(tmp => tmp.Value);

			foreach(string val in FileterEnum)
			{
				if (val!=null && val.Length <= 20 && val.Trim() != "")
				{
					Ret.Add(new Pair<long?, string>(null, val));
				}
			}

			return Ret;
		}

		/// <summary>
		/// LiveInfoIDを取得します｡
		/// もし未登録の場合は､登録した上でIDを返します｡
		/// </summary>
		/// <param name="StationID">StationID</param>
		/// <param name="Caster">Caster</param>
		/// <returns>LiveInfoID</returns>
		private long GetLiveInfoId(string StationID, string Caster)
		{
			ConnectionConsistencyCheck(Con);

			using (SqlCeCommand cmd = Con.CreateCommand())
			{
				cmd.Parameters.Add("@StationID", StationID);
				cmd.Parameters.Add("@Caster", Caster);
				cmd.Parameters.Add("@Time", DateTime.Now);

				cmd.CommandText = "UPDATE LiveInfo SET RefTime=@Time WHERE StationId=@StationId AND Caster=@Caster;";

				if (cmd.ExecuteNonQuery() == 0)
				{
					cmd.CommandText = "INSERT INTO LiveInfo(StationID,Caster,RefTime) VALUES(@StationID,@Caster,@Time);";
					cmd.ExecuteNonQuery();
				}

				cmd.CommandText="SELECT Id FROM LiveInfo WHERE StationId=@StationId AND Caster=@Caster;";

				return (long)cmd.ExecuteScalar();
			}
		}




		/// <summary>
		/// 指定されたStationID及びCasterに関連づけられているマイタグを取得します｡
		/// </summary>
		/// <param name="StationId">StationID</param>
		/// <param name="Caster">Caster</param>
		/// <returns>
		/// 指定されたStationID及びCasterに関連づけられているタグがリストで戻ります｡
		/// 尚､関連づけられているTagが無い場合は､長さ0のリストを戻します｡
		/// </returns>
		public IList<TagModel> GetTag(string StationId, string Caster)
		{
			try
			{
				if (StationId == null)
				{
					throw new ArgumentNullException("StationID");
				}

				if (Caster == null)
				{
					throw new ArgumentNullException("Caster");
				}

				if (StationId == "" || Caster == "")
				{
					throw new ArgumentException();
				}
			}
			catch (Exception)
			{
				return new List<TagModel>();
			}

			ConnectionConsistencyCheck(Con);


			//LiveInfoID確定
			long LiveInfoId = GetLiveInfoId(StationId, Caster);

			return GetTag(LiveInfoId);
		}

		/// <summary>
		/// LiveInfoIDから､それに紐づけられている､Tagの一覧を取得します｡
		/// </summary>
		/// <param name="LiveInfoID">取得対象となるLiveInfoID</param>
		/// <returns>
		/// 取得されたリスト｡
		/// 何もない場合はサイズ0のリストが戻ります｡
		/// </returns>
		private IList<TagModel> GetTag(long LiveInfoID)
		{
			List<TagModel> Ret = new List<TagModel>();

			lamdba1<TagModel, string> CreateTagView = (string TagValue) =>
				{
                    TagModel elemView = new TagModel(TagValue, TagKind.Self);
					return elemView;
				};



			using (SqlCeCommand cmd = Con.CreateCommand())
			{
				cmd.CommandText = 
					"SELECT Value FROM Tags WHERE EXISTS (SELECT * FROM Bind WHERE LiveInfoId=@LiveInfoId AND Tags.Id=Bind.TagsId);";
				cmd.Parameters.Add("@LiveInfoID", LiveInfoID);

				using (SqlCeDataReader rdr = cmd.ExecuteReader())
				{
					while (rdr.Read())
					{
						Ret.Add(CreateTagView(rdr.GetString(0)));
					}
				}
			}

			return Ret;
		}



		#region IDisposable メンバ

		/// <summary>
		/// このクラスのインスタンスを破棄します｡
		/// </summary>
		public void Dispose()
		{
			if (this.Con != null)
			{
				Con.Dispose();
			}
		}

		#endregion
	}
}
